/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.eclipse.core.databinding.observable.gwt.internal.widgets;


import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.impl.DOMImpl;
import com.google.gwt.user.client.ui.DeckPanel;
import com.google.gwt.user.client.ui.HTMLTable;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;

import java.util.Iterator;
import java.util.NoSuchElementException;
// Based on com.google.gwt.user.client.ui.FlexTable
// Style from http://www.imaputz.com/cssStuff/bigFourVersion.html and
// http://rcswebsolutions.wordpress.com/2007/01/02/scrolling-html-table-with-fixed-header/
// Please note: this ScrollTable does not work in IE8
public class ScrollTable extends Panel implements ClickHandler{
  public class FlexTableSection extends TableSection{
    /**
     * FlexTable-specific implementation of {@link HTMLTable.CellFormatter}. The
     * formatter retrieved from {@link HTMLTable#getCellFormatter()} may be cast
     * to this class.
     */
    public class FlexCellFormatter extends CellFormatter {

      /**
       * Gets the column span for the given cell. This is the number of logical
       * columns covered by the cell.
       * 
       * @param row the cell's row
       * @param column the cell's column
       * @return the cell's column span
       * @throws IndexOutOfBoundsException
       */
      public int getColSpan(int row, int column) {
        return DOM.getElementPropertyInt(getElement(row, column), "colSpan");
      }

      /**
       * Gets the row span for the given cell. This is the number of logical rows
       * covered by the cell.
       * 
       * @param row the cell's row
       * @param column the cell's column
       * @return the cell's row span
       * @throws IndexOutOfBoundsException
       */
      public int getRowSpan(int row, int column) {
        return DOM.getElementPropertyInt(getElement(row, column), "rowSpan");
      }

      /**
       * Sets the column span for the given cell. This is the number of logical
       * columns covered by the cell.
       * 
       * @param row the cell's row
       * @param column the cell's column
       * @param colSpan the cell's column span
       * @throws IndexOutOfBoundsException
       */
      public void setColSpan(int row, int column, int colSpan) {
        DOM.setElementPropertyInt(ensureElement(row, column), "colSpan", colSpan);
      }

      /**
       * Sets the row span for the given cell. This is the number of logical rows
       * covered by the cell.
       * 
       * @param row the cell's row
       * @param column the cell's column
       * @param rowSpan the cell's row span
       * @throws IndexOutOfBoundsException
       */
      public void setRowSpan(int row, int column, int rowSpan) {
        DOM.setElementPropertyInt(ensureElement(row, column), "rowSpan", rowSpan);
      }
    }

    FlexTableSection(Element elem){
      super(elem);
      setCellFormatter(new FlexCellFormatter());
      setRowFormatter(new RowFormatter());
      setColumnFormatter(new ColumnFormatter());
    }
    
    /**
     * Gets the number of cells on a given row.
     * 
     * @param row the row whose cells are to be counted
     * @return the number of cells present
     * @throws IndexOutOfBoundsException
     */
    @Override
    public int getCellCount(int row) {
      checkRowBounds(row);
      return super.getDOMCellCount(getBodyElement(), row);
    }

    /**
     * Gets the number of rows.
     * 
     * @return number of rows
     */
    @Override
    public int getRowCount() {
      return getDOMRowCount();
    }

    /**
     * Ensure that the cell exists.
     * 
     * @param row the row to prepare.
     * @param column the column to prepare.
     * @throws IndexOutOfBoundsException if the row is negative
     */
    @Override
    protected void prepareCell(int row, int column) {
      prepareRow(row);
      if (column < 0) {
        throw new IndexOutOfBoundsException(
            "Cannot create a column with a negative index: " + column);
      }

      // Ensure that the requested column exists.
      int cellCount = getCellCount(row);
      int required = column + 1 - cellCount;
      if (required > 0) {
        addCells(getBodyElement(), row, required);
      }
    }

    /**
     * Ensure that the row exists.
     * 
     * @param row The row to prepare.
     * @throws IndexOutOfBoundsException if the row is negative
     */
    @Override
    protected void prepareRow(int row) {
      if (row < 0) {
        throw new IndexOutOfBoundsException(
            "Cannot create a row with a negative index: " + row);
      }

      // Ensure that the requested row exists.
      int rowCount = getRowCount();
      for (int i = rowCount; i <= row; i++) {
        insertRow(i);
      }
    }

    private native void addCells(Element table, int row, int num)/*-{
      var rowElem = table.rows[row];
      for(var i = 0; i < num; i++){
        var cell = $doc.createElement("td");
        rowElem.appendChild(cell);  
      }
    }-*/;
  }

  private class FlexTableHeader extends FlexTableSection{
    FlexTableHeader(Element elem){
      super(elem);
    }
    public void clear(){
      super.clear();
      updateBodyHeight();
    }
    public boolean clearCell(int row, int column) {
      boolean ret = super.clearCell(row, column);
      updateBodyHeight();
      return ret;
    }
    public void setHTML(int row, int column, String html) {
      super.setHTML(row, column, html);
      updateBodyHeight();
    }
    public void setText(int row, int column, String text) {
      super.setText(row, column, text);
      updateBodyHeight();
    }
    public void setWidget(int row, int column, Widget widget) {
      super.setWidget(row, column, widget);
      updateBodyHeight();
    }
    public int insertRow(int beforeRow) {
      int ret = super.insertRow(beforeRow);
      updateBodyHeight();
      return ret;
    }
    public void removeRow(int row) {
      super.removeRow(row);
      updateBodyHeight();
    }
  }
  
  /**
   * This class contains methods used to format a table's columns. It is limited
   * by the support cross-browser HTML support for column formatting.
   */
  public class ColumnFormatter {
    protected Element columnGroup;

    /**
     * Adds a style to the specified column.
     * 
     * @param col the col to which the style will be added
     * @param styleName the style name to be added
     * @see UIObject#addStyleName(String)
     * @throws IndexOutOfBoundsException
     */
    public void addStyleName(int col, String styleName) {
      UIObject.setStyleName(ensureColumn(col), styleName, true);
    }

    /**
     * Gets the style of the specified column.
     * 
     * @param column the column to be queried
     * @return the style name
     * @see UIObject#getStyleName()
     * @throws IndexOutOfBoundsException
     */
    public String getStyleName(int column) {
      return UIObject.getStyleName(ensureColumn(column));
    }

    /**
     * Gets the primary style of the specified column.
     * 
     * @param column the column to be queried
     * @return the style name
     * @see UIObject#getStylePrimaryName()
     * @throws IndexOutOfBoundsException
     */
    public String getStylePrimaryName(int column) {
      return UIObject.getStylePrimaryName(ensureColumn(column));
    }

    /**
     * Removes a style from the specified column.
     * 
     * @param column the column from which the style will be removed
     * @param styleName the style name to be removed
     * @see UIObject#removeStyleName(String)
     * @throws IndexOutOfBoundsException
     */
    public void removeStyleName(int column, String styleName) {
      UIObject.setStyleName(ensureColumn(column), styleName, false);
    }

    /**
     * Sets the style name associated with the specified column.
     * 
     * @param column the column whose style name is to be set
     * @param styleName the new style name
     * @see UIObject#setStyleName(String)
     * @throws IndexOutOfBoundsException
     */
    public void setStyleName(int column, String styleName) {
      UIObject.setStyleName(ensureColumn(column), styleName);
    }

    /**
     * Sets the primary style name associated with the specified column.
     * 
     * @param column the column whose style name is to be set
     * @param styleName the new style name
     * @see UIObject#setStylePrimaryName(String)
     * @throws IndexOutOfBoundsException
     */
    public void setStylePrimaryName(int column, String styleName) {
      UIObject.setStylePrimaryName(ensureColumn(column), styleName);
    }

    /**
     * Sets the width of the specified column.
     * 
     * @param column the column of the cell whose width is to be set
     * @param width the cell's new width, in percentage or pixel units
     * @throws IndexOutOfBoundsException
     */
    public void setWidth(int column, String width) {
      DOM.setElementProperty(ensureColumn(column), "width", width);
    }

    private Element ensureColumn(int col) {
      prepareColumn(col);
      prepareColumnGroup();

      int num = DOM.getChildCount(columnGroup);
      if (num <= col) {
        Element colElement = null;
        for (int i = num; i <= col; i++) {
          colElement = DOM.createElement("col");
          DOM.appendChild(columnGroup, colElement);
        }
        return colElement;
      }
      return DOM.getChild(columnGroup, col);
    }

    /**
     * Prepare the colgroup tag for the first time, guaranteeing that it exists
     * and has at least one col tag in it. This method corrects a Mozilla issue
     * where the col tag will affect the wrong column if a col tag doesn't exist
     * when the element is attached to the page.
     */
    private void prepareColumnGroup() {
      if (columnGroup == null) {
        columnGroup = DOM.createElement("colgroup");
        DOM.insertChild(tableElem, columnGroup, 0);
        DOM.appendChild(columnGroup, DOM.createElement("col"));
      }
    }
  }

  /**
   * Column Formatter.
   */
  private ColumnFormatter columnFormatter;

  private Element scrollTableContainer;
  
  /**
   * Table element.
   */
  private final Element tableElem;
  
  private final TableSection header;
  private final TableSection body;
  private final TableSection footer;

  public ScrollTable() {
    tableElem = DOM.createTable();
    DOM.setStyleAttribute(tableElem, "width", "100%");
    // setElement(tableElem);
    scrollTableContainer = DOM.createDiv();
    DOM.appendChild(scrollTableContainer, tableElem);
    setElement(scrollTableContainer);
    // sinkEvents(Event.ONCLICK);
    
    
    Element theadElem = DOM.createTHead();
    DOM.appendChild(tableElem, theadElem);
    header = createHeaderSection(theadElem);
    // header.prepareRow(0);

    Element bodyElem = DOM.createTBody();
    DOM.appendChild(tableElem, bodyElem);
    body = createBodySection(bodyElem);
    getBody().addClickHandler(this);

    Element tfootElem = DOM.createTFoot();
    DOM.appendChild(tableElem, tfootElem);
    footer = createTFootSection(tfootElem);
    
    DOM.setElementProperty(scrollTableContainer , "className", "gwt-ScrollTableContainer");
    DOM.setElementProperty(tableElem , "className", "gwt-ScrollTable");
  }

  public boolean remove(Widget child) {
    return false;
  }
  public Iterator<Widget> iterator() {
    return new Iterator<Widget>() {
      int nextIndex = -1;
      
      public boolean hasNext() {
        return nextIndex < 1;
      }

      public Widget next() {
        if (!hasNext()) {
          throw new NoSuchElementException();
        }
        if (nextIndex == -1){
          nextIndex++;
          return header;
        }
        if (nextIndex == 0){
          nextIndex++;
          return body;
        }
        return null;
      }

      public void remove() {
      }
    };
  }


  /**
   * Gets the amount of padding that is added around all cells.
   * 
   * @return the cell padding, in pixels
   */
  public int getCellPadding() {
    return DOM.getElementPropertyInt(tableElem, "cellPadding");
  }

  /**
   * Gets the amount of spacing that is added around all cells.
   * 
   * @return the cell spacing, in pixels
   */
  public int getCellSpacing() {
    return DOM.getElementPropertyInt(tableElem, "cellSpacing");
  }

  /**
   * Gets the column formatter.
   * 
   * @return the column formatter
   */
  public ColumnFormatter getColumnFormatter() {
    return columnFormatter;
  }

  /**
   * Sets the width of the table's border. This border is displayed around all
   * cells in the table.
   * 
   * @param width the width of the border, in pixels
   */
  public void setBorderWidth(int width) {
    DOM.setElementProperty(tableElem, "border", "" + width);
  }

  /**
   * Sets the amount of padding to be added around all cells.
   * 
   * @param padding the cell padding, in pixels
   */
  public void setCellPadding(int padding) {
    DOM.setElementPropertyInt(tableElem, "cellPadding", padding);
  }

  /**
   * Sets the amount of spacing to be added around all cells.
   * 
   * @param spacing the cell spacing, in pixels
   */
  public void setCellSpacing(int spacing) {
    DOM.setElementPropertyInt(tableElem, "cellSpacing", spacing);
  }

  public TableSection getHeader(){
    return header;
  }
  public TableSection getBody(){
    return body;
  }
  public TableSection getFooter(){
    return footer;
  }
  protected TableSection createHeaderSection(Element sectionElem){
    return new FlexTableHeader(sectionElem);
  }
  protected TableSection createBodySection(Element sectionElem){
    return new FlexTableSection(sectionElem);
  }
  protected TableSection createTFootSection(Element sectionElem){
    return new FlexTableSection(sectionElem);
  }

  protected void setColumnFormatter(ColumnFormatter formatter) {
    columnFormatter = formatter;
    columnFormatter.prepareColumnGroup();
  }

  /**
   * Subclasses can implement this method. It allows them to decide what to do
   * just before a column is accessed. For classes, such as
   * <code>FlexTable</code>, that do not have a concept of a global column
   * length can ignore this method.
   * 
   * @param column the cell's column
   * @throws IndexOutOfBoundsException
   */
  protected void prepareColumn(int column) {
    // By default, do nothing.
  }


  private boolean isIE(){
    return GWT.create(DOMImpl.class).getClass().getName().equals(
        "com.google.gwt.user.client.impl.DOMImplIE6"
    );
  }

  private boolean updateBodyHeightQueued = false;
  private void updateBodyHeight(){
    if (!updateBodyHeightQueued){
      updateBodyHeightQueued = true;
      DeferredCommand.addCommand(new Command(){
        public void execute() {
          updateBodyHeightQueued = false;
          updateBodyHeightImpl();
        }
      });
    }
  }

  /** NOTE:
   * When added into DeckPanel, the animation changes 'overflow' style attribute
   * We have to set 'overflow' back to 'auto' for scrolling
   */
  private void updateBodyHeightImpl(){
    DOM.setStyleAttribute(scrollTableContainer, "overflow", "auto");
    
    // TODO
    if (!isIE()){
      int containerHeight = scrollTableContainer.getOffsetHeight();
      int headerHeight = header.bodyElem.getOffsetHeight();
      
      DOM.setStyleAttribute(body.bodyElem, "height", (containerHeight - headerHeight - 10) + "px");
    }
  }

  @Override
  public void setHeight(String height) {
    if ((getParent() instanceof DeckPanel) && (height.endsWith("%"))){
      return;
    }
    if (isIE()){
      super.setHeight(height);
    }else{
      DOM.setStyleAttribute(body.bodyElem, "height", height);
    }
    updateBodyHeight();
  }

  private int selectedIndex = -1;
  public void onClick(ClickEvent event) {
    TableSection.Cell cell = getBody().getCellForEvent(event);
    if (cell != null){
      /*
      styleRow(selectedIndex, false);
      selectedIndex = cell.getRowIndex();
      styleRow(selectedIndex, true);
      */
      setSelectedIndex(cell.getRowIndex());
    }
  }
  public void setSelectedIndex(int newSelectedIndex){
    styleRow(selectedIndex, false);
    selectedIndex = newSelectedIndex;
    styleRow(selectedIndex, true);
  }

  private void styleRow(int row, boolean selected){
    if (row < 0){
      return;
    }
    TableSection.RowFormatter rowFormatter = getBody().getRowFormatter();
    
    if (selected){
      rowFormatter.addStyleName(row, "selected");
    }else{
      rowFormatter.removeStyleName(row, "selected");
    }
  }
}
